--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 68ec3479100aaf641d2cba1dbea3c6a137408f57
Parents : 040a204
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-05-18T05:09:36-05:00
feat(map): improve export UI and metadata handling
Changes
4 files changed, 37 insertions(+), 7 deletions(-)
Diff
diff --git a/meshchatx/src/frontend/components/map/MapPage.vue b/meshchatx/src/frontend/components/map/MapPage.vue
index 5e8dd7b6..c912d3af 100644
--- a/meshchatx/src/frontend/components/map/MapPage.vue
+++ b/meshchatx/src/frontend/components/map/MapPage.vue
@@ -237,7 +237,7 @@
<div ref="drawFeatureInfoElement" class="absolute z-45 pointer-events-none">
<div
v-show="drawFeatureInfoPayload"
- class="pointer-events-auto min-w-44 max-w-[min(18rem,calc(100vw-2rem))] rounded-xl border border-gray-200 dark:border-zinc-700 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm shadow-xl px-3 py-2.5 transform -translate-x-1/2 -translate-y-full mb-2"
+ class="info-popup pointer-events-auto min-w-44 max-w-[min(18rem,calc(100vw-2rem))] rounded-xl border border-gray-200 dark:border-zinc-700 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm shadow-xl px-3 py-2.5 transform -translate-x-1/2 -translate-y-full mb-2 overflow-x-hidden"
>
<template v-if="drawFeatureInfoPayload">
<div v-if="drawFeatureInfoPayload.iconSrc" class="flex justify-center mb-2">
@@ -262,23 +262,23 @@
<!-- eslint-disable vue/no-v-html -->
<div
v-else-if="drawFeatureDescriptionSanitized"
- class="text-[11px] text-gray-600 dark:text-zinc-400 prose prose-sm dark:prose-invert max-w-none leading-snug"
+ class="text-[11px] text-gray-600 dark:text-zinc-400 prose prose-sm dark:prose-invert max-w-none leading-snug [&_*]:!bg-transparent [&_*]:!text-inherit"
v-html="drawFeatureDescriptionSanitized"
></div>
<!-- eslint-enable vue/no-v-html -->
<dl
v-if="drawFeatureInfoPayload.extended.length"
- class="mt-2 space-y-1 border-t border-gray-100 dark:border-zinc-800 pt-2"
+ class="mt-2 space-y-1 border-t border-gray-100 dark:border-zinc-800 pt-2 overflow-hidden"
>
<template v-for="row in drawFeatureInfoPayload.extended" :key="row.key">
- <div class="grid grid-cols-[minmax(0,40%)_1fr] gap-x-2 gap-y-0.5 text-[10px]">
+ <div class="grid grid-cols-[minmax(0,40%)_1fr] gap-x-2 gap-y-0.5 text-[10px] min-w-0">
<dt
class="font-semibold text-gray-500 dark:text-zinc-500 truncate"
:title="row.key"
>
{{ row.key }}
</dt>
- <dd class="text-gray-800 dark:text-zinc-200 wrap-break-word m-0">
+ <dd class="text-gray-800 dark:text-zinc-200 truncate m-0" :title="row.value">
{{ row.value }}
</dd>
</div>
@@ -1107,6 +1107,7 @@ import Feature from "ol/Feature";
import Point from "ol/geom/Point";
import * as mdi from "@mdi/js";
import { Style, Text, Fill, Stroke, Circle as CircleStyle, Icon } from "ol/style";
+import { shared as olIconCache } from "ol/style/IconImageCache";
import { fromLonLat, toLonLat } from "ol/proj";
import { defaults as defaultControls } from "ol/control";
import ScaleLine from "ol/control/ScaleLine";
@@ -1420,6 +1421,7 @@ export default {
}
return DOMPurify.sanitize(p.description, {
USE_PROFILES: { html: true },
+ FORBID_ATTR: ["style"],
});
},
},
@@ -1543,6 +1545,8 @@ export default {
this.checkScreenSize();
window.addEventListener("resize", this.checkScreenSize);
+ document.addEventListener("keydown", this.onDeleteKey);
+
// Update info every few seconds
this.reloadInterval = setInterval(() => {
this.fetchTelemetryMarkers();
@@ -1571,6 +1575,7 @@ export default {
}
document.removeEventListener("click", this.handleClickOutside);
window.removeEventListener("resize", this.checkScreenSize);
+ document.removeEventListener("keydown", this.onDeleteKey);
WebSocketConnection.off("message", this.onWebsocketMessage);
if (this._pointerMoveRaf != null) {
cancelAnimationFrame(this._pointerMoveRaf);
@@ -1972,6 +1977,10 @@ export default {
} else {
this.selectedMarker = null;
this.selectedCluster = null;
+ if (feature && this.drawLayer) {
+ this.selectedFeature = markRaw(feature);
+ this.syncDrawFeatureInfoOverlay();
+ }
}
// Deselect drawing if clicking empty space
@@ -3294,6 +3303,7 @@ export default {
this.drawSource.on("removefeature", persist);
this.drawSource.on("changefeature", persist);
this.drawSource.on("clear", persist);
+ this.drawSource.on("clear", () => olIconCache.clear());
},
deleteSelectedFeature() {
@@ -3670,6 +3680,20 @@ export default {
}
this.closeContextMenu();
},
+ onDeleteKey(e) {
+ if (e.key !== "Delete" && e.key !== "Backspace") return;
+ if (
+ e.target &&
+ (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.isContentEditable)
+ )
+ return;
+ const f = this.selectedFeature;
+ if (!f || f.get("telemetry")) return;
+ this.drawSource.removeFeature(f);
+ this.selectedFeature = null;
+ this.syncDrawFeatureInfoOverlay();
+ this.saveMapState();
+ },
contextAddNote() {
if (this.contextMenuFeature) {
this.startEditingNote(this.contextMenuFeature);
diff --git a/meshchatx/src/frontend/components/map/internal/MapExportConfigPanel.vue b/meshchatx/src/frontend/components/map/internal/MapExportConfigPanel.vue
index 7532ef8e..9220909a 100644
--- a/meshchatx/src/frontend/components/map/internal/MapExportConfigPanel.vue
+++ b/meshchatx/src/frontend/components/map/internal/MapExportConfigPanel.vue
@@ -2,7 +2,7 @@
<template>
<div
- class="absolute top-4 left-1/2 -translate-x-1/2 z-20 w-80 bg-white dark:bg-zinc-900 rounded-xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden text-gray-900 dark:text-zinc-100"
+ class="absolute top-0 mt-14 left-1/2 -translate-x-1/2 z-20 w-80 bg-white dark:bg-zinc-900 rounded-xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden text-gray-900 dark:text-zinc-100"
>
<div class="p-4 border-b border-gray-200 dark:border-zinc-800 flex items-center justify-between">
<h3 class="font-semibold text-gray-900 dark:text-zinc-100">{{ $t("map.export_area") }}</h3>
diff --git a/meshchatx/src/frontend/components/map/internal/MapExportInstructions.vue b/meshchatx/src/frontend/components/map/internal/MapExportInstructions.vue
index 5c5fc6e8..ef307368 100644
--- a/meshchatx/src/frontend/components/map/internal/MapExportInstructions.vue
+++ b/meshchatx/src/frontend/components/map/internal/MapExportInstructions.vue
@@ -1,7 +1,7 @@
<!-- SPDX-License-Identifier: 0BSD -->
<template>
- <div class="absolute inset-x-0 top-4 z-20 flex justify-center px-2 pointer-events-none">
+ <div class="absolute inset-x-0 top-0 mt-14 z-20 flex justify-center px-2 pointer-events-none">
<div class="flex flex-col items-center gap-2 max-w-[min(100vw-2rem,36rem)] w-full pointer-events-auto">
<div
class="px-4 py-2 bg-blue-600 text-white rounded-full shadow-lg font-medium text-sm animate-bounce text-center"
diff --git a/meshchatx/src/frontend/js/mapExchange/metadataUtils.js b/meshchatx/src/frontend/js/mapExchange/metadataUtils.js
index 485be91d..6408c709 100644
--- a/meshchatx/src/frontend/js/mapExchange/metadataUtils.js
+++ b/meshchatx/src/frontend/js/mapExchange/metadataUtils.js
@@ -24,6 +24,8 @@ const SKIP_EXTENDED = new Set([
"segmentKind",
"bearingMetrics",
"_measureOverlay",
+ "styleUrl",
+ "style",
MCX_ICON_DATA_URL,
MCX_ICON_HREF,
MCX_ICON_SCALE,
@@ -152,9 +154,13 @@ export function getDrawFeatureMetadataPayload(feature) {
} else {
vs = String(v);
}
+ vs = vs.trim();
if (vs.length > 400) {
vs = `${vs.slice(0, 400)}…`;
}
+ if (!vs || vs === "null" || vs === "undefined") {
+ continue;
+ }
extended.push({ key: k, value: vs });
}
extended.sort((a, b) => a.key.localeCompare(b.key));
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────